package io.gitee.hejiang.jersey;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.sql.DataSource;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.mozilla.javascript.DefiningClassLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.util.StreamUtils;

import com.alibaba.druid.pool.DruidDataSource;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.fasterxml.jackson.jaxrs.xml.JacksonJaxbXMLProvider;

import io.swagger.config.FilterFactory;
import io.swagger.core.filter.SwaggerSpecFilter;
import io.swagger.inflector.config.Configuration;
import io.swagger.inflector.controllers.InflectResultController;
import io.swagger.inflector.controllers.SwaggerResourceController;
import io.swagger.inflector.converters.Converter;
import io.swagger.inflector.converters.InputConverter;
import io.swagger.inflector.models.InflectResult;
import io.swagger.inflector.processors.EntityProcessor;
import io.swagger.inflector.processors.EntityProcessorFactory;
import io.swagger.inflector.processors.JacksonProcessor;
import io.swagger.inflector.processors.JsonExampleProvider;
import io.swagger.inflector.processors.JsonNodeExampleSerializer;
import io.swagger.inflector.processors.JsonProvider;
import io.swagger.inflector.processors.XMLExampleProvider;
import io.swagger.inflector.processors.YamlExampleProvider;
import io.swagger.inflector.utils.DefaultContentTypeProvider;
import io.swagger.inflector.utils.DefaultSpecFilter;
import io.swagger.inflector.utils.ResolverUtil;
import io.swagger.inflector.validators.Validator;
import io.swagger.jaxrs.listing.SwaggerSerializers;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.Swagger;
import io.swagger.parser.SwaggerParser;
import io.swagger.parser.util.SwaggerDeserializationResult;
import io.swagger.util.Json;
import io.swagger.util.Yaml;

/**
 * ResourceConfig for Jersey web application
 * 
 * Jersey 通过 ResourceConfig 配置和定义 web 应用程序
 */
public class JerseyResourceConfig extends ResourceConfig {
	private static final Logger LOGGER = LoggerFactory.getLogger(JerseyResourceConfig.class);

	protected Configuration config;
	protected ObjectMapper objectMapper;
	protected ApplicationContext applicationContext;

	private String originalBasePath;

	private Map<String, List<String>> missingOperations = new HashMap<String, List<String>>();
	private Set<String> unimplementedMappedModels = new TreeSet<String>();

	private ClassLoader loader = new DefiningClassLoader();
	private String uploadFilePath;
	private String jdbcDriverPath;

	public JerseyResourceConfig(ApplicationContext applicationContext, Configuration configuration) {
		this(applicationContext, configuration, Json.mapper());
	}

	public JerseyResourceConfig(ApplicationContext applicationContext, Configuration configuration,
			ObjectMapper objectMapper) {
		this.applicationContext = applicationContext;
		this.objectMapper = objectMapper;
		this.setUploadFilePath(
				applicationContext.getEnvironment().getProperty("spring.jersey.upload-file-path", "./upload/"));
		this.setJdbcDriverPath(
				applicationContext.getEnvironment().getProperty("spring.jersey.jdbc-driver-path", "./jdbc/"));
		init(configuration);
	}

	protected ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	protected ObjectMapper getObjectMapper() {
		return objectMapper;
	}

	public Configuration getInflectorConfiguration() {
		return this.config;
	}

	public ClassLoader getCustomClassLoader() {
		return this.loader;
	}

	@SuppressWarnings("unchecked")
	protected void init(Configuration configuration) {
		config = configuration;
		try {
			new File(this.getUploadFilePath()).mkdirs();
		} catch (Exception e) {
			LOGGER.error("创建上传文件目录 {} 失败: {}", this.getUploadFilePath(), e.getLocalizedMessage());
		}

		SwaggerDeserializationResult swaggerParseResult = new SwaggerParser().readWithInfo(config.getSwaggerUrl(), null,
				true);
		Swagger swagger = swaggerParseResult.getSwagger();

		if (!config.getValidatePayloads().isEmpty()) {
			LOGGER.info("resolving swagger");
			new ResolverUtil().resolveFully(swagger);
		}

		if (swagger != null) {
			originalBasePath = swagger.getBasePath();
			String rootPath = configuration.getRootPath();
			if (originalBasePath.startsWith(rootPath)) {
				originalBasePath = originalBasePath.substring(rootPath.length());
				swagger.setBasePath(originalBasePath);
			}

			// 初始化数据源
			Map<String, Object> dsconfig = (Map<String, Object>) swagger.getVendorExtensions().get("x-datasource");
			DataSource datasource = constructDataSource(dsconfig);

			JerseyReflectionUtils utils = new JerseyReflectionUtils();
			utils.setConfiguration(config);
			utils.setClassLoader(loader);
			utils.constructModels(swagger.getDefinitions());

			Map<String, Path> paths = swagger.getPaths();
			for (String pathString : paths.keySet()) {
				Path path = paths.get(pathString);
				final Resource.Builder builder = Resource.builder();

				builder.path(basePath(originalBasePath, pathString));
				Operation operation;

				operation = path.getGet();
				if (operation != null) {
					addOperation(pathString, builder, HttpMethod.GET, operation, swagger, datasource);
				}
				operation = path.getPost();
				if (operation != null) {
					addOperation(pathString, builder, HttpMethod.POST, operation, swagger, datasource);
				}
				operation = path.getPut();
				if (operation != null) {
					addOperation(pathString, builder, HttpMethod.PUT, operation, swagger, datasource);
				}
				operation = path.getDelete();
				if (operation != null) {
					addOperation(pathString, builder, HttpMethod.DELETE, operation, swagger, datasource);
				}
				operation = path.getOptions();
				if (operation != null) {
					addOperation(pathString, builder, HttpMethod.OPTIONS, operation, swagger, datasource);
				}
				operation = path.getPatch();
				if (operation != null) {
					addOperation(pathString, builder, "PATCH", operation, swagger, datasource);
				}
				registerResources(builder.build());
			}
		} else {
			LOGGER.error("No swagger definition detected!  Not much to do...");
		}

		// filters
		initSwaggerSpecFilter();

		// Content providers
		registerContentProviders(swagger);

		// multipart feature
		register(new MultiPartFeature());

		// Swagger serializers
		register(SwaggerSerializers.class);
		SwaggerSerializers.setPrettyPrint(config.isPrettyPrint());

		// register exception mapper
		for (Class<?> exceptionMapper : config.getExceptionMappers()) {
			register(exceptionMapper);
		}

		// validators
		initInputValidators();

		// converters
		initInputConverters();

		// finalization
		postInitialization(swaggerParseResult);
	}

	/**
	 * @param configuration
	 * @param swaggerParseResult
	 */
	protected void postInitialization(SwaggerDeserializationResult swaggerParseResult) {
		InflectResult result = new InflectResult();
		for (String key : swaggerParseResult.getMessages()) {
			result.specParseMessage(key);
		}
		for (String key : missingOperations.keySet()) {
			result.unimplementedControllers(key, missingOperations.get(key));
		}
		for (String model : config.getUnimplementedModels()) {
			result.unimplementedModel(model);
		}
		for (String model : unimplementedMappedModels) {
			result.unimplementedModel(model);
		}

		if (Configuration.Environment.DEVELOPMENT.equals(config.getEnvironment())) {
			if (missingOperations.size() > 0) {
				LOGGER.debug("There are unimplemented operations!");
			}
			for (String key : missingOperations.keySet()) {
				LOGGER.debug(key);
				for (String val : missingOperations.get(key)) {
					LOGGER.debug(" - " + val);
				}
			}
			final Resource.Builder builder = Resource.builder();
			builder.path(basePath(originalBasePath, config.getSwaggerBase() + "debug.json")).addMethod(HttpMethod.GET)
					.produces(MediaType.APPLICATION_JSON).handledBy(new InflectResultController(result)).build();

			registerResources(builder.build());
		} else if (Configuration.Environment.STAGING.equals(config.getEnvironment())) {
			if (missingOperations.size() > 0) {
				LOGGER.warn("There are unimplemented operations!");
			}
			for (String key : missingOperations.keySet()) {
				LOGGER.warn(key);
				for (String val : missingOperations.get(key)) {
					LOGGER.warn(" - " + val);
				}
			}
		} else if (Configuration.Environment.PRODUCTION.equals(config.getEnvironment())) {
			if (missingOperations.size() > 0) {
				LOGGER.error("There are unimplemented operations!");
			}
			for (String key : missingOperations.keySet()) {
				LOGGER.error(key);
				for (String val : missingOperations.get(key)) {
					LOGGER.error(" - " + val);
				}
			}
			if (missingOperations.size() > 0) {
				LOGGER.error("Unable to start due to unimplemented methods");
				throw new RuntimeException("Unable to start due to unimplemented methods");
			}
		}
	}

	protected void initInputConverters() {
		if (config.getInputConverters() != null && config.getInputConverters().size() > 0) {
			InputConverter.getInstance().getConverters().clear();
			for (String converter : config.getInputConverters()) {
				try {
					String clsName = converter;
					if ("defaultConverter".equalsIgnoreCase(converter)) {
						clsName = "io.swagger.inflector.converters.DefaultConverter";
					}
					LOGGER.debug("adding converter `" + clsName + "`");
					InputConverter.getInstance().addConverter((Converter) Class.forName(clsName).newInstance());
				} catch (Exception e) {
					LOGGER.warn("unable to add validator `" + converter + "`");
				}
			}
		} else {
			InputConverter.getInstance().defaultConverters();
		}
	}

	/**
	 * 
	 */
	protected void initInputValidators() {
		if (config.getInputValidators() != null && config.getInputValidators().size() > 0) {
			InputConverter.getInstance().getValidators().clear();
			for (String inputValidator : config.getInputValidators()) {
				try {
					String clsName = inputValidator;
					if ("requiredFieldValidator".equalsIgnoreCase(inputValidator)) {
						clsName = "io.swagger.inflector.validators.DefaultValidator";
					}
					if ("numericValidator".equalsIgnoreCase(inputValidator)) {
						clsName = "io.swagger.inflector.validators.NumericValidator";
					}
					if ("stringValidator".equalsIgnoreCase(inputValidator)) {
						clsName = "io.swagger.inflector.validators.StringTypeValidator";
					}
					InputConverter.getInstance().addValidator((Validator) Class.forName(clsName).newInstance());
				} catch (Exception e) {
					LOGGER.warn("unable to add validator `" + inputValidator + "`");
					e.printStackTrace();
				}
			}
		} else {
			InputConverter.getInstance().defaultValidators();
		}
	}

	protected void registerContentProviders(Swagger swagger) {
		// Add content providers in order or appearance in the configuration
		SimpleModule simpleModule = new SimpleModule();
		simpleModule.addSerializer(new JsonNodeExampleSerializer());
		for (String item : config.getEntityProcessors()) {
			if ("json".equalsIgnoreCase(item)) {
				// JSON
				getObjectMapper().registerModule(simpleModule);
				register(JacksonJsonProvider.class);
				register(JsonExampleProvider.class);

				// If a custom object mapper has this INDENT_OUTPUT specified
				// already,
				// disable the the JsonProvider as it's redundant
				if (!getObjectMapper().isEnabled(SerializationFeature.INDENT_OUTPUT)) {
					register(new JsonProvider(config.isPrettyPrint()));
				}
				if (!isRegistered(DefaultContentTypeProvider.class)) {
					register(new DefaultContentTypeProvider(MediaType.APPLICATION_JSON_TYPE), ContextResolver.class);
				}
				enableProcessor(JacksonProcessor.class, MediaType.APPLICATION_JSON_TYPE);
				enableSwaggerJSON(swagger, config.getSwaggerProcessors());
			} else if ("xml".equalsIgnoreCase(item)) {
				// XML
				if (!isRegistered(DefaultContentTypeProvider.class)) {
					register(new DefaultContentTypeProvider(MediaType.APPLICATION_XML_TYPE), ContextResolver.class);
				}
				enableProcessor(JacksonProcessor.class, MediaType.APPLICATION_XML_TYPE);
				register(JacksonJaxbXMLProvider.class);
				register(XMLExampleProvider.class);
			} else if ("yaml".equalsIgnoreCase(item)) {
				// YAML
				Yaml.mapper().registerModule(simpleModule);
				register(YamlExampleProvider.class);
				enableProcessor(JacksonProcessor.class, JacksonProcessor.APPLICATION_YAML_TYPE);
				enableSwaggerYAML(swagger, config.getSwaggerProcessors());
			}
		}
	}

	protected void initSwaggerSpecFilter() {
		if (config.getFilterClass() != null) {
			if (!config.getFilterClass().isEmpty()) {
				try {
					FilterFactory.setFilter(
							(SwaggerSpecFilter) this.getClassLoader().loadClass(config.getFilterClass()).newInstance());
				} catch (Exception e) {
					LOGGER.error("Unable to set filter class " + config.getFilterClass());
				}
			}
		} else {
			FilterFactory.setFilter(new DefaultSpecFilter());
		}
	}

	public static String basePath(String basePath, String path) {
		if (StringUtils.isBlank(basePath)) {
			basePath = "/";
		}
		if (!basePath.endsWith("/") && !"/".equals(basePath) && StringUtils.isBlank(path)) {
			basePath = basePath + "/";
		}
		if (StringUtils.isEmpty(path)) {
			return basePath;
		}
		if (path.equals("/")) {
			return basePath + "/";
		}
		if (!path.endsWith("/")) {
			path = path + "/";
		}
		if (!path.startsWith("/")) {
			path = "/" + path;
		}
		if ("/".equals(basePath)) {
			basePath = "";
		}
		if (basePath.endsWith("/") && path.startsWith("/")) {
			path = path.substring(1);
		}
		return basePath + path;
	}

	private void enableProcessor(Class<?> cls, MediaType type) {
		List<EntityProcessor> processors = EntityProcessorFactory.getProcessors();
		for (EntityProcessor processor : processors) {
			if (processor.getClass().equals(cls)) {
				processor.enableType(type);
				return;
			}
		}
		try {
			EntityProcessor processor = (EntityProcessor) cls.newInstance();
			processor.enableType(type);
		} catch (Exception e) {
			LOGGER.error("unable to initialize class " + cls);
		}
	}

	private void enableSwaggerJSON(Swagger swagger, List<String> swaggerProcessors) {
		final Resource.Builder builder = Resource.builder();
		builder.path(
				basePath(originalBasePath, StringUtils.appendIfMissing(config.getSwaggerBase(), "/") + "swagger.json"))
				.addMethod(HttpMethod.GET).produces(MediaType.APPLICATION_JSON)
				.handledBy(new SwaggerResourceController(swagger, swaggerProcessors)).build();

		registerResources(builder.build());
	}

	private void enableSwaggerYAML(Swagger swagger, List<String> swaggerProcessors) {
		final Resource.Builder builder = Resource.builder();
		builder.path(
				basePath(originalBasePath, StringUtils.appendIfMissing(config.getSwaggerBase(), "/") + "swagger.yaml"))
				.addMethod(HttpMethod.GET).produces("application/yaml")
				.handledBy(new SwaggerResourceController(swagger, swaggerProcessors)).build();

		registerResources(builder.build());
	}

	protected void addOperation(String pathString, Resource.Builder builder, String method, Operation operation,
			Swagger swagger, DataSource datasource) {
		LOGGER.debug("adding operation for `" + pathString + "` " + method);
		JerseyOperationInflector controller = new JerseyOperationInflector(this, pathString, method, operation, swagger,
				datasource);

		unimplementedMappedModels.addAll(controller.getUnimplementedMappedModels());
		builder.addMethod(method).handledBy(controller);
	}

	protected DataSource constructDataSource(Map<String, Object> config) {
		File jdbcDir = new File(jdbcDriverPath);
		List<File> jars = jdbcDir.exists() ? Arrays.asList(jdbcDir.listFiles()) : new ArrayList<>();

		URL[] urls = new URL[jars.size()];
		for (int i = 0; i < jars.size(); i++) {
			try {
				urls[i] = jars.get(i).toURI().toURL();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		URLClassLoader driverClassLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());

		DruidDataSource datasource = new DruidDataSource();
		datasource.setDriverClassLoader(driverClassLoader);
		try {
			BeanUtils.populate(datasource, config);
			datasource.addFilters("com.alibaba.druid.filter.stat.StatFilter");
			LOGGER.info("Datasource constructed.");
		} catch (Exception e) {
			LOGGER.error("Fail to load datasource.", e);
		}

		// 初始化 petstore 数据库
		String url = (String) config.get("url");
		if (url.startsWith("jdbc:h2:mem:petstore;")) {
			InputStream stream = this.getClassLoader().getResourceAsStream("petstore.sql");
			if (stream != null) {
				try (Connection connection = datasource.getConnection();
						Statement statement = connection.createStatement();) {
					String sql = StreamUtils.copyToString(stream, Charset.forName("UTF-8"));
					statement.execute(sql);
				} catch (SQLException | IOException e) {
					LOGGER.warn("Fail to execute petstore.sql.");
				}
			}
		}

		return datasource;
	}

	public String getUploadFilePath() {
		return uploadFilePath;
	}

	public void setUploadFilePath(String uploadFilePath) {
		this.uploadFilePath = uploadFilePath;
	}

	public String getJdbcDriverPath() {
		return jdbcDriverPath;
	}

	public void setJdbcDriverPath(String jdbcDriverPath) {
		this.jdbcDriverPath = jdbcDriverPath;
	}
}